/* 
 * File:   Raytracer.cpp
 * Author: brady
 * 
 * Created on June 23, 2011, 6:02 PM
 */
#define _USE_MATH_DEFINES
#include <core/Raytracer.h>
#include <pthread.h>
#include <cmath>
#include <Windows.h>
#include <random>


void Raytracer::render(){
    //create a pixel dispenser

    PixelDispenser pixelDispenser = PixelDispenser(gParams.xResolution, gParams.yResolution);

    //create an array of threads
    pthread_t* threads = new pthread_t[gParams.threadCount];

    //create thread params
    ThreadParams threadParams = ThreadParams();
    threadParams.pixelDispenser = &pixelDispenser;

    //create individual threads
    for(int t = 0; t<gParams.threadCount; t++){
        pthread_create(&threads[t], NULL, renderThread, &threadParams);
    }

    //wait for them to complete checking progress periodically
    bool finished = false;
    while(!finished){
        pthread_mutex_lock(&PIXEL_DISPENSER_MUTEX);
        if(!pixelDispenser.isEmpty()){
            cout << "Rendering... " << (int) (100.0 * pixelDispenser.completion()) << "% complete \r";
            cout.flush();
        }
        else{
            cout << "Rendering... 100% complete\n";
            finished = true;
        }
        pthread_mutex_unlock(&PIXEL_DISPENSER_MUTEX);
        //this should not update too often. checking progress takes cpu time from
        //the actual rendering threads.
        Sleep(1);
    }
    
    //join all the threads
    for(int t = 0; t<gParams.threadCount; t++){
        pthread_join(threads[t], NULL);
    }
    
    //done!

}

void*  Raytracer::renderThread(void* threadParams){
    //really, i'm just popping the stack
    ThreadParams* tp = (ThreadParams*)threadParams;
    PixelDispenser& pixelDispenser = *tp->pixelDispenser;
    Scene* scene = gParams.scene;
    Pixel currentPixel;
    while(true){
        //lock the pixelDispenser and get a pixel
        pthread_mutex_lock(&PIXEL_DISPENSER_MUTEX);
        if(!pixelDispenser.isEmpty()){
            currentPixel = pixelDispenser.getPixel();
        }
        else{
            //terminate thread
            pthread_mutex_unlock(&PIXEL_DISPENSER_MUTEX);
            pthread_exit(NULL);
            return NULL;
        }
        pthread_mutex_unlock(&PIXEL_DISPENSER_MUTEX);
        vector<Ray> rayBatch = generateRays(currentPixel);
        Vector3D finalPixelColor(0,0,0);
        int samplesCast = 0;
        for(int i=0; i<rayBatch.size(); i++){
            IntersectionParams intersectionParams(rayBatch[i], 0, 100.0); //pull these out into constants
            IntersectionResult intersectionResult = scene->intersectionStructure->getFirstIntersection(intersectionParams);
            if(intersectionResult.shape != NULL){
                finalPixelColor += intersectionResult.shape->getShader()->shade(intersectionResult);
            }
            else{
                finalPixelColor += gParams.scene->backgroundColor;
            }
            samplesCast++;
        }
        finalPixelColor /= samplesCast;
        finalPixelColor.cap(1.0);
        colorPixel(currentPixel, finalPixelColor);
    }
}

vector<Ray> Raytracer::generateRays(Pixel& pixel){
    vector<Ray> rays;
    //grab references to parameters
    const Camera& camera = gParams.scene->camera;
    //the number of rows and columns to divide the pixel into
    int pixelDivisions = (int) sqrt((float)gParams.initialSamples);
    int xResolution = gParams.xResolution;
    int yResolution = gParams.yResolution;
    int x = pixel.x;
    int y = pixel.y;
    int batchNum = y * xResolution + x;

    double left, top, frameWidth, frameHeight;
    frameWidth = camera.planeWidth;
    frameHeight = frameWidth / ((double)xResolution/(double)yResolution);
    left = -frameWidth/2.0;
    top = -frameHeight/2.0;

    double pxlWidth = frameWidth/xResolution;
    double pxlHeight = frameHeight/yResolution;

    double directionX, directionY, directionZ;
    //left edge + x pixels + half a pixel
    directionX = left + pxlWidth*(x+0.5);
    //top edge + y pixels + half a pixel
    directionY = top + pxlHeight*(y+0.5);
    directionZ = -camera.focalLength;
    Vector3D pixelCenter((camera.u * directionX) +
                         (camera.v * directionY) +
                         (camera.w * directionZ) );

    //if we want 1 sample, just return one ray through the center
    if(gParams.initialSamples == 1){
        pixelCenter.normalize();
        Ray r = Ray(camera.origin, pixelCenter, gParams.recursionLimit);
        r.batchNumber = batchNum;
        r.sampleNumber = 1;
        rays.push_back(r);
        return rays;
    }
    //otherwise, jitter them evenly over the pixel
    double subPixelWidth = pxlWidth/pixelDivisions;
    double subPixelHeight = pxlHeight/pixelDivisions;
    StochasticRNG srng;
    srng.generateRandomSet(gParams.initialSamples);
    
    for(int i=0; i<gParams.initialSamples; i++){
        double xOffset = (rand()/RAND_MAX)*subPixelWidth;
        double yOffset = (rand()/RAND_MAX)*subPixelHeight;
        int sp_row = i/pixelDivisions;
        int sp_col = i%pixelDivisions;
        double sp_x = left + x*pxlWidth + sp_col*subPixelWidth + xOffset;
        double sp_y = top + y*pxlHeight + sp_row*subPixelHeight + yOffset;

        //a random point on the lense

        double theta = ((i/(double)gParams.initialSamples) +
                        (rand() * (1.0/gParams.initialSamples)/(double)RAND_MAX))
                        *2*M_PI;

        //lense size is diameter of lense
        //nth root pushes radius towards 1
        //this counter's the algorithm's tendancy
        //to favor the center of the circle

        double radius = pow((rand()/(double)RAND_MAX), (.5))*camera.lenseSize*0.5;

        double lenseX = cos(theta)*radius;
        double lenseY = sin(theta)*radius;

        Vector3D sampleOrigin = camera.origin + (camera.u * lenseX) + (camera.v * lenseY);

        //this is the 3D point the ray will pass through the image plane at
        Vector3D planePoint((camera.u * sp_x) +
                                   (camera.v * sp_y) +
                                   (camera.w * -camera.focalLength) + camera.origin);

        Vector3D sampleDirection = planePoint - sampleOrigin;

        Ray sample = Ray(sampleOrigin, sampleDirection.normalize(), gParams.recursionLimit);
        sample.batchNumber = batchNum;
        sample.sampleNumber = srng.getRandomSet()[i];
        rays.push_back(sample);
    }
    
    return rays;
}

void Raytracer::colorPixel(const Pixel& pixel, const Vector3D& color)
{
    int xRes = gParams.xResolution;
    int index = (pixel.y * xRes * 3) + pixel.x * 3;
    gParams.imageData[index + 0] = (float) color[0];
    gParams.imageData[index + 1] = (float) color[1];
    gParams.imageData[index + 2] = (float) color[2];
}
